package game.ui;

import game.configuration.Configuration;
import game.domain.AsteroidField;
import game.domain.SpaceShip;
import game.statistics.Score;
import game.statistics.Statistics;
import game.Game;

import javax.swing.*;
import java.awt.*;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;

public class FullScreenGamePanel extends JPanel implements Runnable {
	private static final int FONT_SIZE = 24;
	private static final Color INFO_MESSAGE_COLOUR = Color.blue;
	private static int MAX_FRAME_SKIPS = 5;
	private static final int NO_DELAYS_PER_YIELD = 16;
	private static final int NUMBER_OF_ASTEROIDS = 10;

	// no. of frames that can be skipped in any one animation loop i.e the games state is updated but not rendered number of FPS values stored to get an average
	private Thread animationThread;
	private volatile boolean animationThreadIsRunning = false;
	private final AsteroidField asteroidField;

	private final Background background;
	private Image doubleBufferingImage = null;
	private boolean finishedOff = false;
	private final Font font;
	private final FontMetrics fontMetrics;
	private volatile boolean gameOver = false;
	private volatile boolean isPaused = false;
	private Graphics2D offScreenRenderer;
	private final int panelHeight;

	private final int panelWidth;
	private final long periodBetweenDrawingInNanos;

	private final Score score;
	private final SpaceShip spaceShip;

	private final Statistics statistics;

	public FullScreenGamePanel(Game game, long period) {
		Configuration.configure(false);

		statistics = new Statistics();
		periodBetweenDrawingInNanos = period;
		final Toolkit toolKit = Toolkit.getDefaultToolkit();
		final Dimension screenDimensions = toolKit.getScreenSize();
		panelWidth = screenDimensions.width;
		panelHeight = screenDimensions.height;
		background = new Background(panelWidth, panelHeight);
		setBackground(background.getColour());
		setPreferredSize(screenDimensions);
		setFocusable(true);
		// the JPanel now has focus, so receives key events
		requestFocus();
		readyForTermination();

		asteroidField = new AsteroidField(panelWidth, panelHeight, NUMBER_OF_ASTEROIDS);
		spaceShip = new SpaceShip(panelWidth, panelHeight);
		score = new Score();

		font = new Font("SansSerif", Font.BOLD, FONT_SIZE);
		fontMetrics = getFontMetrics(font);
	}

	@Override
	public void addNotify() {
		// wait for the JPanel to be added to the JFrame before starting creates
		// the peer
		super.addNotify();
		if (animationThread == null || !animationThreadIsRunning) {
			// start the thread
			animationThread = new Thread(this);
			animationThread.start();
		}
	}

	private void finishOff() {
		// Tasks to do before terminating. Called at end of run() and via the shutdown hook in readyForTermination(). The call at the end of run() is
		// not really necessary, but included for safety. The flag stops the code being called twice.
		if (!finishedOff) {
			finishedOff = true;
			printStats();
			System.exit(0);
		}
	}

	private void gameOverMessage(Graphics g) {
		final String msg = "Game Over. Your Score: " + score;
		final int x = (panelWidth - fontMetrics.stringWidth(msg)) / 2;
		final int y = (panelHeight - fontMetrics.getHeight()) / 2;
		g.setColor(Color.red);
		g.setFont(font);
		g.drawString(msg, x, y);
	}

	private void gameRender() {
		if (doubleBufferingImage == null) {
			doubleBufferingImage = createImage(panelWidth, panelHeight);
			if (doubleBufferingImage == null) {
				System.out.println("dbImage is null");
				return;
			} else {
				offScreenRenderer = (Graphics2D) doubleBufferingImage.getGraphics();
			}
		}
		background.draw(offScreenRenderer);
		offScreenRenderer.setColor(INFO_MESSAGE_COLOUR);
		offScreenRenderer.setFont(font);
		renderStatistics();
		if (gameOver) {
			gameOverMessage(offScreenRenderer);
		} else {
			asteroidField.draw(offScreenRenderer);
			spaceShip.draw(offScreenRenderer);
		}
		score.draw(offScreenRenderer, (int) spaceShip.getPosition().x, (int) (spaceShip.getPosition().y + 50));
	}

	private void gameUpdate() {
		if (!isPaused && !gameOver) {
			if (asteroidField.size() == 0) {
				gameOver = true;
			} else {
				asteroidField.removeAsteroidsThatCollideWithOtherAsteroids();

				if (spaceShip.getLaser() != null && !spaceShip.getLaser().isDisabled()) {
					if (asteroidField.removeAsteroidsThatCollide(spaceShip.getLaser())) {
						spaceShip.laserHit();
						score.increment();
					}
				}
				spaceShip.animate();
				asteroidField.animate();
				if (spaceShip.getLaser() != null) {
					spaceShip.getLaser().animate();
				}
				gameOver = asteroidField.doesAnythingCollide(spaceShip);
			}
		}
	}

	private boolean isQuitKeyPressed(KeyEvent e, final int keyCode) {
		return keyCode == KeyEvent.VK_ESCAPE || keyCode == KeyEvent.VK_Q || keyCode == KeyEvent.VK_END || keyCode == KeyEvent.VK_C && e.isControlDown();
	}

	private void paintScreen() {
		// use active rendering to put the buffered image on-screen
		try {
			Graphics2D graphics = (Graphics2D) getGraphics();
			if (graphics != null && doubleBufferingImage != null) {
				graphics.drawImage(doubleBufferingImage, 0, 0, null);
			}
			Toolkit.getDefaultToolkit().sync();
			graphics.dispose();

		} catch (final Exception e) {
			System.out.println("Graphics error: " + e);
		}
	}

	private void printStats() {
		System.out.println(statistics.getStats());
	}

	private void readyForTermination() {
		addKeyListener(new KeyAdapter() {
			@Override
			public void keyPressed(KeyEvent e) {
				final int keyCode = e.getKeyCode();
				if (isQuitKeyPressed(e, keyCode)) {
					animationThreadIsRunning = false;
				} else {
					Configuration configuration = Configuration.getInstance();
					switch (keyCode) {
						case KeyEvent.VK_DOWN:
							spaceShip.decellerate();
							break;
						case KeyEvent.VK_UP:
							spaceShip.accellerate();
							break;
						case KeyEvent.VK_LEFT:
							spaceShip.turnLeft();
							break;
						case KeyEvent.VK_RIGHT:
							spaceShip.turnRight();
							break;
						case KeyEvent.VK_SPACE:
							spaceShip.fireLaser();
							break;
						case KeyEvent.VK_D:
							configuration.setIsDebugEnabled(!configuration.isDebugEnabled());
							break;
					}
				}
			}
		});

		// for shutdown tasks a shutdown may not only come from the program
		Runtime.getRuntime().addShutdownHook(new Thread() {
			@Override
			public void run() {
				animationThreadIsRunning = false;
				finishOff();
			}
		});
	}

	private void renderStatistics() {
		offScreenRenderer.drawString(statistics.getStatsLine(), 20, 25);
		offScreenRenderer.drawString(statistics.getTimeSpentLine(), 10, panelHeight - 15);
	}

	public void run() {
		long overSleepTime = 0L;
		int noDelays = 0;
		long excess = 0L;
		statistics.setGameStartTime(System.nanoTime());
		long beforeTime = statistics.getGameStartTime();
		animationThreadIsRunning = true;
		while (animationThreadIsRunning) {
			gameUpdate();
			// render the game to a buffer
			gameRender();
			// draw the buffer on-screen
			paintScreen();
			long afterTime = System.nanoTime();
			long timeDiff = afterTime - beforeTime;
			long sleepTime = periodBetweenDrawingInNanos - timeDiff - overSleepTime;
			if (sleepTime > 0) {
				// some time left in this cycle
				try {
					Thread.sleep(sleepTime / 1000000L); // nano -> ms
				} catch (final InterruptedException ex) {
				}
				overSleepTime = System.nanoTime() - afterTime - sleepTime;
			} else {
				// sleepTime <= 0; the frame took longer than the periodBetweenDrawingInNanos store
				// excess time value
				excess -= sleepTime;
				overSleepTime = 0L;
				if (++noDelays >= NO_DELAYS_PER_YIELD) {
					// give another thread a chance to run
					Thread.yield();
					noDelays = 0;
				}
			}
			beforeTime = System.nanoTime();
			// If frame animation is taking too long, update the game state
			// without rendering it, to get the updates/sec nearer to the
			// required FPS.
			int skips = 0;
			while (excess > periodBetweenDrawingInNanos && skips < MAX_FRAME_SKIPS) {
				excess -= periodBetweenDrawingInNanos;
				// update state but don't render
				gameUpdate();
				skips++;
			}
			statistics.addSkippedFrames(skips);
			// excess = excess % periodBetweenDrawingInNanos;
			statistics.storeStats(periodBetweenDrawingInNanos);
		}
		finishOff();
	}
}